﻿using Jacobi.Vst.Framework;
using Jacobi.Vst.Core;
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using Aleator.Resources;
using Aleator.Helpers;

namespace Aleator.Core
{
    /// <summary>
    /// Rhythm (chord playing) phrase class
    /// </summary>
    public class RhythmPhrase : ConcretePhrase
    {
        #region Members

        private List<ConcreteNote> m_chordNotes;

        private int m_chordResolution;
        private int m_chordTotal;

        private bool m_rigid;

        private static Chord.Token[] SecondTokens = { Chord.Token.Add2, Chord.Token.Dom7Sus2, Chord.Token.Dom7Sus4Add2, Chord.Token.M6Sus2, Chord.Token.M7Sus2, Chord.Token.M9Sus2, Chord.Token.mAdd2, Chord.Token.mSus2, Chord.Token.mSus4Add2,
            Chord.Token.Sus2, Chord.Token.Sus4Add2, Chord.Token.MSus2 };
        private static Chord.Token[] FourthTokens = { Chord.Token.Add4, Chord.Token.Dom7Sus4, Chord.Token.Dom9Sus4, Chord.Token.M6Sus4, Chord.Token.m7Sus4, Chord.Token.M7Sus4, Chord.Token.mSus4, Chord.Token.Sus4, Chord.Token.Sus4Add4, Chord.Token.MSus4};
        private static Chord.Token[] SixthTokens = { Chord.Token.m6, Chord.Token.M6, Chord.Token.M6Sus2, Chord.Token.M6Sus4, Chord.Token.mb6, Chord.Token.Dimb6 };
        private static Chord.Token[] SeventhTokens = { Chord.Token.Dim7, Chord.Token.Dom7, Chord.Token.HalfDim7, Chord.Token.M7, Chord.Token.m7, Chord.Token.mM7 };
        private static Chord.Token[] NinthTokens = { Chord.Token.mAdd9, Chord.Token.Dom9, Chord.Token.Dom9Sus4, Chord.Token.m7b5b9, Chord.Token.m7b9, Chord.Token.M9, Chord.Token.m9, Chord.Token.mAdd9, Chord.Token.Ninth, Chord.Token.Sus4Add9 };
        private static Chord.Token[] ThirteenthTokens = { Chord.Token.maj13, Chord.Token.m13, Chord.Token.m7b9b13, Chord.Token.maj13Sharp11, Chord.Token.Thirteenth, Chord.Token.m7b13, Chord.Token.Dim7b9b13 };
        private static Chord.Token[] SuspendedTokens = { Chord.Token.Dom7Sus2, Chord.Token.Dom7Sus4, Chord.Token.Dom7Sus4Add2, Chord.Token.Dom9Sus4, Chord.Token.M6Sus2, Chord.Token.M6Sus4, Chord.Token.M7Sus2, Chord.Token.m7Sus4,
            Chord.Token.M7Sus4, Chord.Token.M9Sus2, Chord.Token.mSus2, Chord.Token.mSus4, Chord.Token.mSus4Add2, Chord.Token.Sus2, Chord.Token.Sus4, Chord.Token.Sus4Add2, Chord.Token.Sus4Add4, Chord.Token.Sus4Add9, Chord.Token.MSus4, Chord.Token.MSus2 };
        private static Chord.Token[] NoThreeTokens = { Chord.Token.M7no3 };

        #endregion

        #region Constructors

        /// <summary>
        /// Main constructor
        /// </summary>
        /// <param name="progression">Progression this phrase is being attachd to</param>
        /// <param name="basephrase">Original to phrase to use as template for copying</param>
        public RhythmPhrase(ConcreteProgression progression, RhythmPhrase basephrase = null)
        {
            m_progression = progression;

            Notes = new List<ConcreteNote>();
            m_chordNotes = new List<ConcreteNote>();

            if (basephrase == null)
            {
                ChordResolution = Session.RndGenerator.Next(1, 4);
                SetTotals();

                m_resolution = 2;

                if (!m_rigid)
                {
                    m_noteDistribution = Distribution.Tight;
                    Tight();
                }
                else
                {
                    m_noteDistribution = Distribution.Rigid;
                    Rigid();
                }
            }
            else
            {
                ChordResolution = basephrase.m_chordResolution;
                CopyToPhrase((ConcretePhrase)basephrase);
            }

            Progression.Notes.AddRange(m_notes);
        }

        /// <summary>
        /// Rhythm phrase constructor allowing for variance from a progression group
        /// </summary>
        /// <param name="progression"></param>
        /// <param name="basephrase"></param>
        public RhythmPhrase(ConcreteProgression progression, int variance)
        {
            m_progression = progression;

			//If there is no basephrase, we are generating original note lists. Otherwise, initiate copying procedure
			if (!progression.Composition.Progressions.Any())
			{
				throw new NotImplementedException("Variance requires at least one progression in the current composition!");
			}
			else
				Commonality.Explode(this);

            Progression.Notes.AddRange(m_notes);
        }
        #endregion

        #region Properties

        /// <summary>
        /// For chords, resolution is how many plays per whole note
        /// </summary>
        public int ChordResolution
        {
            get
            {
                return m_chordResolution;
            }
            set
            {
                switch (value)
                {
                    case 3:
                        m_chordResolution = 4;
                        break;
                    default:
                        m_chordResolution = value;
                        break;
                }
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Set total number of notes in the list
        /// </summary>
        /// <param name="sigma">Standard deviation for chord total</param>
        protected override void SetTotals(double sigma = 2.0)
        {
            if (Session.RndGenerator.Next(1, 3) == 1)
            {
                m_rigid = false;
                base.SetTotals();
                m_chordTotal = GetTotal(m_mu, m_sigma);
            }
            else
                m_rigid = true;

            m_noteOnMessageNumber = Convert.ToInt16(AppResources.RhythmOnMessageID);
            m_noteOffMessageNumber = Convert.ToInt16(AppResources.RhythmOffMessageID);
        }

        /// <summary>
        /// 
        /// </summary>
        protected override void Tight()
        {
            m_continuous.Beta = PlayableNotesInMeasure - 1;
            m_continuous.Alpha = 0;

            m_rayleigh.Sigma = 3.0;

            //inner counter will be re-used to add remaining notes into the phrase below
            int chordsInMeasure = m_chordTotal / m_progression.TotalMeasures;

            if (chordsInMeasure > PlayableNotesInMeasure)
                chordsInMeasure = PlayableNotesInMeasure;

            int j = 0;
            int beatbase = 0;
            int counter = 0;

            Chord c;

            m_measure = 0;

            //Fill out the first measure of the phrase
            while (j < chordsInMeasure)
            {
                GetBeat();

                //If the number returned by the generator is within the desired range and has not already been selected,
                //We can add it it to the note array
                if (!m_notes.Where(k => k != null && k.Location == m_location && k.Measure == 0).Any())
                {
                    //Pick a note from the appropriate chord in the phrase
                    //TODO: Make sure we are accounting for octaves - we'll stay within 3 to mimic the bass guitar
                    try
                    {
                        c = Progression.Chords.Where(ch => ch.Key < m_location / m_resolution).OrderBy(ch => ch.Key)
                            .Select(ch => ch.Value).First();
                    }
                    catch
                    {
                        c = Progression.Chords.Where(ch => ch.Key == 0).Select(ch => ch.Value).First();
                    }

                    PlayChord(c);

                    j++;
                }
            }

            //Fill out the rest
            for (int i = 1; i < m_progression.TotalMeasures; i++)
            {
			 m_measure = i;
                beatbase = i * m_progression.Numerator;

                if (j > m_progression.TotalMeasures * chordsInMeasure)
                    break;
                try
                {
                    for (int k = 0; k < chordsInMeasure; k++)
                    {
                        //WTF is this.
                        if (k == 0)
                            m_location = m_notes[0].Location;
                        else
                            m_location = m_notes[(k * 3)].Location;

                        try
                        {
                            c = Progression.Chords.Where(ch => ch.Key < beatbase + (m_location / m_resolution)).OrderByDescending(ch => ch.Key)
                                .Select(ch => ch.Value).First();
                        }
                        catch
                        {
                            c = Progression.Chords.Where(ch => ch.Key == 0).Select(ch => ch.Value).First();
                        }

                        PlayChord(c);

                        j++;
                    }
                }
                catch (IndexOutOfRangeException ior)
                {
                    Debug.WriteLine(ior.Message);
                }
            }

            //Get any beat that isn't in the kick collection yet
            List<int> beats = new List<int>();
            List<ConcreteNote> fullnotes = m_notes.Where(k => k != null).ToList();

            try
            {
                for (int i = 0; i < PlayableNotesInMeasure; i++)
                {
                    if (fullnotes.Where(k => k.Location == i).Count() == 0)
                        beats.Add(i);
                }
            }
            catch (NullReferenceException nre)
            {
                Debug.WriteLine("Error message: " + nre.Message);
            }

            if (beats.Count > 0)
            {
                m_location = beats[Session.RndGenerator.Next(beats.Count)];

                //Add the remaining notes to the array to fill it up using the same process
                while (j < m_noteTotal)
                {
                    m_measure = Session.RndGenerator.Next(m_progression.TotalMeasures);

                    if (counter == m_progression.TotalMeasures)
                    {
                        counter = 0;
                        m_location = beats[Session.RndGenerator.Next(beats.Count)];
                    }

                    if (!m_notes.Where(k => k != null && k.Location == m_location && k.Measure == m_measure).Any())
                    {
                        beatbase = m_measure * m_progression.Numerator;

                        try
                        {
                            c = Progression.Chords.Where(ch => ch.Key < beatbase + (m_location / m_resolution)).OrderByDescending(ch => ch.Key)
                                .Select(ch => ch.Value).First();
                        }
                        catch
                        {
                            c = Progression.Chords.Where(ch => ch.Key == 0).Select(ch => ch.Value).First();
                        }

                        PlayChord(c);

                        j++;
                    }

                    counter++;
                }
            }

            AddRests();
        }
        
        /// <summary>
        /// This method will produce a chord phrase that will play on almost every quantized note. Limited variation in notes
        /// </summary>
        protected override void Rigid()
        {
            //First we need to determine where the missing notes fall
            int missingbeat = Session.RndGenerator.Next(PlayableNotesInMeasure);

            Chord c;
            Chord.ScaleDegree degree = Chord.ScaleDegree.I;

            ConcreteNote n = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote o = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote p = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote q = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote r = (ConcreteNote)m_notefactory.GetNote();

            int beatbase;

            //Iterate through all the measures
            for (int m = 0; m < Progression.TotalMeasures; m++)
            {
                beatbase = m_progression.Numerator * m;

                //Iterate through the playable notes in a measure
                for (int b = 0; b < PlayableNotesInMeasure; b++)
                {
                    //If this beat/measure combo wasnt selected for omission, we can add it
                    if (missingbeat != b)
                    {
                        try
                        {
                            c = Progression.Chords.Where(ch => ch.Key < beatbase + (b / m_resolution)).OrderByDescending(ch => ch.Key)
                                .Select(ch => ch.Value).First();
                        }
                        catch
                        {
                            c = Progression.Chords.Where(ch => ch.Key == 0).Select(ch => ch.Value).First();
                        }

                        degree = PlayChord(c, degree, beatbase, m, b);
                    }
                }
            }

            AddRests();
        }

        private void PlayChord(Chord c)
        {
            ConcreteNote n = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote o = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote p = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote q = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote r = (ConcreteNote)m_notefactory.GetNote();

            if (!c.IsInverted)
            {
                m_chordNotes.Add(c.Notes.Where(k => c.Roots.Contains(k)).First());
                int octave = m_chordNotes[0].Octave;

                //m_chordNotes.Add(c.Notes.Where(k => c.Thirds.Contains(k)).First());
                n = m_chordNotes[0];
                m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, m_measure, n, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));

                //Don't attempt to play a 3rd in a suspended chord. it will throw an exception
                if (SuspendedTokens.Contains(c.ChordToken))
                {
                    if (SecondTokens.Contains(c.ChordToken))
                        m_chordNotes.Add(c.Notes.Where(k => c.Ninths.Contains(k) && k.NoteID > n.NoteID).First());
                    else
                        m_chordNotes.Add(c.Notes.Where(k => c.Fourths.Contains(k) && k.NoteID > n.NoteID).First());
                }
                else
                    m_chordNotes.Add(c.Notes.Where(k => c.Thirds.Contains(k)).OrderByDescending(k => k.NoteID).First());

                m_chordNotes.Add(c.Notes.Where(k => c.Fifths.Contains(k) && k.NoteID > n.NoteID).First());

                o = m_chordNotes[1];
                p = m_chordNotes[2];

                if (!NoThreeTokens.Contains(c.ChordToken) && c.ChordToken != Chord.Token.Fifth)
                    m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, m_measure, o, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));

                m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, m_measure, p, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));

                if (SixthTokens.Contains(c.ChordToken))
                {
                    m_chordNotes.Add(c.Notes.Where(l => c.Sixths.Contains(l) && l.NoteID > n.NoteID).First());
                    q = m_chordNotes[3];
                    m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, m_measure, q, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                }

                if (SeventhTokens.Contains(c.ChordToken))
                {
                    m_chordNotes.Add(c.Notes.Where(k => c.Sevenths.Contains(k) && k.NoteID > n.NoteID).First());
                    q = m_chordNotes[3];
                    m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, m_measure, q, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                }

                if (NinthTokens.Contains(c.ChordToken))
                {
                    m_chordNotes.Add(c.Notes.Where(k => c.Sevenths.Contains(k) && k.NoteID > n.NoteID).First());
                    m_chordNotes.Add(c.Notes.Where(k => c.Ninths.Contains(k)).OrderByDescending(k => k.NoteID).First());

                    q = m_chordNotes[3];
                    r = m_chordNotes[4];
                    m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, m_measure, q, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                    m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, m_measure, r, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                }

                if (ThirteenthTokens.Contains(c.ChordToken))
                {
                    m_chordNotes.Add(c.Notes.Where(k => c.Sevenths.Contains(k) && k.NoteID > n.NoteID).First());
                    m_chordNotes.Add(c.Notes.Where(k => c.Sixths.Contains(k)).OrderByDescending(k => k.NoteID).First());

                    q = m_chordNotes[3];
                    r = m_chordNotes[4];
                    m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, m_measure, q, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                    m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, m_measure, r, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                }

            }
            else
            {
                //Inversion numbers need to be added to XML/XSD before this is possible
            }

            Debug.WriteLine(string.Format("Adding {0} chord: {1},{2},{3} | Measure: {4}, Location: {5}", c.DisplayName, n.Name, o.Name, p.Name, m_measure, m_location));

            m_chordNotes.Clear();
        }

        private Chord.ScaleDegree PlayChord(Chord c, Chord.ScaleDegree degree, int beatbase, int measure, int beat)
        {
            ConcreteNote n = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote o = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote p = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote q = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote r = (ConcreteNote)m_notefactory.GetNote();

            if (!c.IsInverted)
            {

                //Only change notes if we are changing chords as well
                if (c.Degree != degree || m_chordNotes.Count == 0)
                {
                    m_chordNotes.Add(c.Notes.Where(k => c.Roots.Contains(k)).First());
                    n = m_chordNotes[0];

                    //Don't attempt to play a 3rd in a suspended chord. it will throw an exception
                    if (SuspendedTokens.Contains(c.ChordToken))
                    {
                        if (SecondTokens.Contains(c.ChordToken))
                            m_chordNotes.Add(c.Notes.Where(k => c.Ninths.Contains(k) && k.NoteID > n.NoteID).First());
                        else
                            m_chordNotes.Add(c.Notes.Where(k => c.Fourths.Contains(k) && k.NoteID > n.NoteID).First());
                    }
                    else
                        m_chordNotes.Add(c.Notes.Where(k => c.Thirds.Contains(k)).OrderByDescending(k => k.NoteID).First());

                    m_chordNotes.Add(c.Notes.Where(k => c.Fifths.Contains(k) && k.NoteID > n.NoteID).First());

                    o = m_chordNotes[1];
                    p = m_chordNotes[2];

                    if (SixthTokens.Contains(c.ChordToken))
                    {
                        m_chordNotes.Add(c.Notes.Where(l => c.Sixths.Contains(l) && l.NoteID > n.NoteID).First());
                        q = m_chordNotes[3];
                    }

                    if (SeventhTokens.Contains(c.ChordToken))
                    {
                        m_chordNotes.Add(c.Notes.Where(l => c.Sevenths.Contains(l) && l.NoteID > n.NoteID).First());
                        q = m_chordNotes[3];
                    }

                    //For extended chords, just grab the highest available for now.
                    if (NinthTokens.Contains(c.ChordToken))
                    {
                        m_chordNotes.Add(c.Notes.Where(l => c.Sevenths.Contains(l) && l.NoteID > n.NoteID).First());
                        m_chordNotes.Add(c.Notes.Where(k => c.Ninths.Contains(k)).OrderByDescending(k => k.NoteID).First());

                        q = m_chordNotes[3];
                        r = m_chordNotes[4];
                    }

                    if (ThirteenthTokens.Contains(c.ChordToken))
                    {
                        m_chordNotes.Add(c.Notes.Where(l => c.Sevenths.Contains(l) && l.NoteID > n.NoteID).First());
                        m_chordNotes.Add(c.Notes.Where(k => c.Sixths.Contains(k)).OrderByDescending(k => k.NoteID).First());

                        q = m_chordNotes[3];
                        r = m_chordNotes[4];
                    }

                    ChordInterval = c.GetChordInterval(n);
                    m_chordNotes.Clear();
                }

                m_notes.Add((ConcreteNote)m_notefactory.GetNote(beat, measure, n, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));

                if (!NoThreeTokens.Contains(c.ChordToken) && c.ChordToken != Chord.Token.Fifth)
                    m_notes.Add((ConcreteNote)m_notefactory.GetNote(beat, measure, o, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));

                m_notes.Add((ConcreteNote)m_notefactory.GetNote(beat, measure, p, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));

                if (SeventhTokens.Contains(c.ChordToken))
                    m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, measure, q, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));

                if (NinthTokens.Contains(c.ChordToken) || ThirteenthTokens.Contains(c.ChordToken))
                {
                    m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, measure, q, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                    m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, measure, r, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                }
            }
            else
            {
                //Inversion numbers need to be added to XML/XSD before this is possible
            }

            Debug.WriteLine(string.Format("Adding {0} chord: {1},{2},{3} | Measure: {4}, Location: {5}", c.DisplayName, n.Name, o.Name, p.Name, measure, beat));


            return c.Degree;
        }

        private void FillRemainingMeasures(bool inverted)
        {

        }

        private void CleanUp(bool inverted)
        {

        }

        /// <summary>
        /// Insert rests into the progression by setting the noteoff locations for all notes
        /// </summary>
        protected override void AddRests()
        {
            //Sort note list (sort is done by beat value)
            Notes.Sort();

            //Group the note list by location
            var result = Notes.GroupBy(a => a.Location);

            int total = result.Count();
            int resultcounter = 0;
            int nextlocation;
            int off;
            int spacer;

            //For each group, randomly pick a number less than the gap value and play the notes for that duration
            foreach (var group in result)
            {
                //Get the location of the next group and set the offset accordingly
                if (resultcounter < result.Count() - 1)
                {
                    var nextGroup = result.ElementAt(resultcounter + 1);
                    nextlocation = nextGroup.ElementAt(0).Location;
                    spacer = nextlocation - group.ElementAt(0).Location;
                }
                else
                {
                    nextlocation = result.ElementAt(0).ElementAt(0).Location;
                    spacer = (PlayableNotesInMeasure - group.ElementAt(0).Location) + nextlocation;
                }

                if (spacer < 2)
                    spacer = 2;

                off = Session.RndGenerator.Next(1, spacer);

                //Go through all of the notes with this gap length and set the off note values
                foreach (ConcreteNote n in group)
                {
                    n.OffLocation = n.Location + off;

                    //If the offbeat to the appropriate beat in the next measure. Otherwise the note is turned off in the same measure.
                    if (n.OffLocation >= PlayableNotesInMeasure - 1)
                    {
                        if (n.Measure == Progression.TotalMeasures - 1)
                        {
                            n.OffLocation = PlayableNotesInMeasure - 1;
                            n.OffMeasure = n.Measure;
                        }
                        else
                        {
                            n.OffLocation -= PlayableNotesInMeasure - 1;
                            n.OffMeasure = n.Measure + 1;
                        }
                    }
                    else
                        n.OffMeasure = n.Measure;
                }

                resultcounter++;
            }
        }

        #endregion
    }
}
